#!/usr/bin/env python
"""
Copyright 2007 William Rapha

This program is a color timer which may be used in a Toastmasters meeting for example

Requires wxPython 2.6 or newer

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2 as
published by the Free Software Foundation.
"""

import wx, string, os, time, threading, platform, math, wave, array, tempfile, sys
from AppKit import NSSound

TITLE = "Toastmasters timer"

MINIMUM_COLOR   = wx.GREEN
MAXIMUM_COLOR   = wx.RED
MIDDLE_COLOR    = wx.Colour(255, 180, 0) # Orange

TIMER_EVENT     = wx.NewId()
STARTBUTTON     = wx.NewId()
RESUMEBUTTON    = wx.NewId()
STOPBUTTON      = wx.NewId()

class MainFrame(wx.Frame):

    def __init__(self, parent, id, title):
        # First, call the base class' __init__ method to create the frame
        wx.Frame.__init__(self, parent, id, title)
        self._top = wx.GetTopLevelParent(self)

        # Add a panel and some controls to display the size and position
        panel = wx.Panel(self, -1)
        self.minimumLabel       = wx.StaticText(panel, -1, "Minimum time (in minutes):")
        self.maximumLabel       = wx.StaticText(panel, -1, "Maximum time (in minutes):")
        self.minimumTimeCtrl    = wx.TextCtrl(panel, -1, "")
        self.maximumTimeCtrl    = wx.TextCtrl(panel, -1, "")
        timeLabel               = wx.StaticText(panel, -1, "Time :")
        self.elapsedTimeLabel   = wx.StaticText(panel, -1, "")

        startButton             = wx.Button(panel, STARTBUTTON, "Start")
        stopButton              = wx.Button(panel, STOPBUTTON, "Stop")
        resumeButton            = wx.Button(panel, RESUMEBUTTON, "Resume")

        self.Bind(wx.EVT_BUTTON, self.OnStart, id=STARTBUTTON)
        self.Bind(wx.EVT_BUTTON, self.OnResume,  id=RESUMEBUTTON)
        self.Bind(wx.EVT_BUTTON, self.OnStop,  id=STOPBUTTON)

        self.panel = panel

        # Use some sizers for layout of the widgets
        sizer = wx.FlexGridSizer(4, 2, 5, 5)
        sizer.Add(self.minimumLabel)
        sizer.Add(self.minimumTimeCtrl)
        sizer.Add(self.maximumLabel)
        sizer.Add(self.maximumTimeCtrl)
        sizer.Add(timeLabel)
        sizer.Add(self.elapsedTimeLabel)

        buttonRow = wx.BoxSizer(wx.HORIZONTAL)
        buttonRow.Add(startButton, flag=wx.RIGHT, border=5)
        buttonRow.Add(stopButton)
        sizer.Add(buttonRow)
        sizer.Add(resumeButton)

        border = wx.BoxSizer()
        border.Add(sizer, 0, wx.ALL, 15)
        panel.SetSizerAndFit(border)
        self.Fit()

        width = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_X)
        height = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y)
        self.Maximize(True)
        self.panel.SetBackgroundColour(wx.BLACK)
        self.panel.Refresh()
        self.setLabelsForegroundColor(wx.WHITE)

        self.timer = wx.Timer()
        self.timer.SetOwner(self, TIMER_EVENT)
        wx.EVT_TIMER(self, TIMER_EVENT, self.OnTimer)

        self.wavFilePath = generateWavFile(0.15, 465, 50)

        self.minimumTimeCtrl.SetFocus()

        self.Bind(wx.EVT_CLOSE, self.OnQuit)

    def OnStart(self, event):

        self.OnStop(event)

        self.setLabelsForegroundColor(wx.WHITE)
        self.panel.SetBackgroundColour(wx.BLACK)
        self.panel.Refresh()

        try:
            self.minimum = int(self.minimumTimeCtrl.GetValue()) * 60
        except Exception, e:
            print e
            return

        try:
            self.maximum = int(self.maximumTimeCtrl.GetValue()) * 60
        except Exception, e:
            print e
            return
        
        self.middle = (self.minimum + self.maximum) / 2
            
        self.elapsedTime = -1
        self.timer.Start(1000)
        self.OnTimer(event)
        
    def OnStop(self, event):
        self.timer.Stop()

    def OnResume(self, event):
        self.timer.Start(1000)

    def secondsToCaption(self, seconds):
        return "%2d:%02d" % (seconds / 60, seconds % 60)

    def setLabelsForegroundColor(self, colour):
        for label in (self.minimumLabel, self.maximumLabel, self.elapsedTimeLabel):
            label.SetForegroundColour(colour)

    def OnTimer(self, event):
        self.elapsedTime = self.elapsedTime + 1
        self.elapsedTimeLabel.SetLabel(self.secondsToCaption(self.elapsedTime))
        self._top.SetTitle("%s %s" % (self.secondsToCaption(self.elapsedTime), TITLE))
        
        if self.elapsedTime == self.minimum:
            self.setLabelsForegroundColor(wx.BLACK)
            self.panel.SetBackgroundColour(MINIMUM_COLOR)
            self.panel.Refresh()
            self.beep()
        elif self.elapsedTime == self.middle:
            self.panel.SetBackgroundColour(MIDDLE_COLOR)
            self.panel.Refresh()
            self.beep()
        elif self.elapsedTime == self.maximum:
            self.panel.SetBackgroundColour(MAXIMUM_COLOR)
            self.panel.Refresh()
            self.beep()

    def beepThread(self):
        if self.elapsedTime == self.minimum:
            times = 1
        elif self.elapsedTime == self.middle:
            times = 2
        elif self.elapsedTime == self.maximum:
            times = 3

        for i in range(times):
            self.playWavFile(self.wavFilePath)
            time.sleep(0.2)

    def playWavFile(self, filepath):
        if sys.platform == "darwin":
            self.playWavFileMacOSX(filepath)
        else:
            wx.Sound(filepath).Play()

    def playWavFileMacOSX(self, filepath):
        sound = NSSound.alloc()
        sound.initWithContentsOfFile_byReference_(filepath, True)
        sound.play()
        while True:
            if not sound.isPlaying():
                time.sleep(0.1)
                break

    def beep(self):
        # Audio feedback for accessibility
        threading.Thread(target=self.beepThread).start()

    def OnQuit(self, event):
        os.remove(self.wavFilePath)
        self.Destroy()
        

class ToastmastersTimer(wx.App):

    def OnInit(self):

        frame = MainFrame(None, -1, TITLE)
        frame.Show(True)
        if os.path.exists("timer.ico"):
            frame.SetIcon(wx.Icon("timer.ico", type=wx.BITMAP_TYPE_ICO))
        self.SetTopWindow(frame)

        return True


def generateWavFile(seconds, frequency, amplitudePercentOfMax):
    # This function Copyright Bengt Richter 2002
    # cf. http://mail.python.org/pipermail/python-list/2002-December/177097.html

    tmpFile = tempfile.NamedTemporaryFile(mode="w")
    tmpFilePath = tmpFile.name
    tmpFile.close()

    #init output and related vars
    SAMPLESPERSECOND = 44100
    wavFile = wave.open(tmpFilePath, "w")
    wavFile.setnchannels(1)
    wavFile.setsampwidth(2) # 2 bytes for 32767 amplitude
    wavFile.setframerate(SAMPLESPERSECOND)
    wavFile.setcomptype("NONE", "Uncompressed")

    # calculate frequency as cycles/sample
    cyclesPerSample = float(frequency)/SAMPLESPERSECOND
    numberOfSamples = int(seconds*SAMPLESPERSECOND)
    maxAmplitude = (amplitudePercentOfMax/100.)*32767
    sampleArray = array.array('h')

    for nSamples in xrange(numberOfSamples):
        cycles = nSamples*cyclesPerSample
        cycles -= int(cycles) # makes 0.0<=cycles<1.0
        sampleValue = int(round(maxAmplitude*math.sin(2.0*math.pi*cycles))) # round 
        sampleArray.append(sampleValue)

    wavFile.writeframes(sampleArray.tostring())
    wavFile.close()
    return tmpFilePath

if __name__ == "__main__":
    app = ToastmastersTimer(0)
    app.MainLoop()

# vi: ts=4 sw=4 et
